﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net.Forums
{
    using System;
    using System.Collections.Generic;
    using System.Data.Linq;
    using System.Data.Linq.Mapping;
    using System.IO;
    using System.Linq;
    using System.Net;
    using Microsoft.Phone.Data.Linq.Mapping;

    /// <summary>Represents the article database.</summary>
    internal sealed class Database : DataContext
    {
        private readonly FileIOProvider fileIOProvider;
        private readonly Cache cache;
        private readonly ICryptoProvider cryptoProvider;
        private readonly Root root;
        private readonly Dictionary<Action, bool> afterCommitActions = new Dictionary<Action, bool>();
        private bool reopenAfterCommit;
        private bool isDisposed;

        private readonly Dictionary<Type, ITable> tables = new Dictionary<Type, ITable>(6);
#if !WINDOWS_PHONE
        private Table<Version> versionTable;
#endif
        private Table<Root> rootTable;
        private Table<Host> hostTable;
        private Table<OutboundArticle> outboundArticleTable;
        private Table<Group> groupTable;
        private Table<InboundArticle> inboundArticleTable;
        private Table<DeleteFile> deleteFileTable;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="DataContext.SubmitChanges(ConflictMode)"/>.</summary>
        /// <remarks>Converts exceptions and performs actions scheduled for after commit.</remarks>
        public sealed override void SubmitChanges(ConflictMode failureMode)
        {
            this.AssertNotDisposedCache();

            try
            {
                base.SubmitChanges(failureMode);
            }
            catch (Exception ex)
            {
                // On the phone we get a nice SqlCeException, just like on the desktop. Problem is, we don't have access
                // to that type and (due to partial trust on the phone) cannot even get the NativeError property through
                // reflection. So it seems the only way how we can distinguish between the different errors is by
                // switching on ex.Message.
                switch (ex.Message.Substring(0, 22))
                {
                    case "The column cannot cont":
                        throw new ArgumentNullException(null, ex);
                    case "A duplicate value cann":
                        throw new ArgumentException(
                            "A constraint was violated, see InnerException for more information.", ex);
                    case "The data was truncated": // desktop
                    case "String truncation: max": // phone
                    case "Byte array truncation ": // phone
                        throw new ArgumentException(
                            "The length of a character or byte sequence exceeds the maximum allowed length.", ex);
                    default:
                        throw;
                }
            }

            foreach (var action in this.afterCommitActions.ToList())
            {
                this.afterCommitActions.Remove(action.Key);
                action.Key();
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal Database(Cache cache, FileIOProvider fileIOProvider, ICryptoProvider cryptoProvider) :
            this(fileIOProvider)
        {
            using (var baseGuard = ScopeGuard.Create(this))
            {
                if (cryptoProvider == null)
                {
                    throw new ArgumentNullException("cryptoProvider");
                }

                this.cache = cache;
                this.cryptoProvider = cryptoProvider;

                if (!this.DatabaseExists())
                {
                    throw new FileNotFoundException("Database does not exist: " + GetConnectionString(fileIOProvider));
                }

                try
                {
                    this.root = this.RootTable.Single();
                }
                catch (InvalidOperationException ex)
                {
                    throw new IncompatibleCacheException("Unexpected number of rows in the Root table.", ex);
                }
                catch (InvalidCastException ex)
                {
                    throw Root.GetIncompatibleException(ex);
                }
                catch (Exception ex)
                {
                    Root.HandleSqlCeException(ex);
                    throw;
                }

                this.root.Init(this);
                baseGuard.Dismiss();
            }
        }

        internal Database Clone()
        {
            return new Database(this.cache, this.fileIOProvider, this.cryptoProvider);
        }

        internal Host AddHost<TProvider, TConnection>(
            string name, int port, string userName, string emailAddress, string accountName, string password)
            where TProvider : IForumProvider<TConnection>, new()
            where TConnection : Connection
        {
            this.AssertNotDisposedCache();

            if ((port < IPEndPoint.MinPort) || (port > IPEndPoint.MaxPort))
            {
                throw new ArgumentOutOfRangeException("port", "Must be >= 0 and <= 65535.");
            }

            var host = new Host()
            {
                Root = this.root,
                Provider = typeof(TProvider).AssemblyQualifiedName,
                Name = name,
                Port = port,
                UserName = userName,
                EmailAddress = emailAddress,
                AccountName = accountName,
                Password = password
            };

            this.InsertOnSubmit(host);
            return host;
        }

        internal IEnumerable<IHost> Hosts
        {
            get
            {
                this.AssertNotDisposedCache();
                return this.HostTable.Cast<IHost>();
            }
        }

        internal IEnumerable<IOutboundArticle> GetOutboundArticles(SendStatus sendStatus)
        {
            this.AssertNotDisposedCache();
            return this.root.GetOutboundArticles(sendStatus);
        }

        internal IEnumerable<IOutboundArticle> GetDeletedOutboundArticles()
        {
            this.AssertNotDisposedCache();
            return this.root.GetDeletedOutboundArticles();
        }

        internal IDictionary<SendStatus, int> GetOutboundArticleCounts()
        {
            this.AssertNotDisposedCache();

            var counts = new Dictionary<SendStatus, int>()
            {
                { SendStatus.Draft, 0 }, { SendStatus.Outbox, 0 }, { SendStatus.Sent, 0 }
            };

            foreach (var count in this.root.GetOutboundArticleCounts())
            {
                counts[count.Key] = count.Value;
            }

            return counts;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal FileIOProvider FileIOProvider
        {
            get { return this.fileIOProvider; }
        }

        internal ICryptoProvider CryptoProvider
        {
            get { return this.cryptoProvider; }
        }

#if !WINDOWS_PHONE
        internal Table<Version> VersionTable
        {
            get { return this.GetTable(ref this.versionTable); }
        }
#endif

        internal Table<Root> RootTable
        {
            get { return this.GetTable(ref this.rootTable); }
        }

        internal Table<Host> HostTable
        {
            get { return this.GetTable(ref this.hostTable); }
        }

        internal Table<OutboundArticle> OutboundArticleTable
        {
            get { return this.GetTable(ref this.outboundArticleTable); }
        }

        internal Table<Group> GroupTable
        {
            get { return this.GetTable(ref this.groupTable); }
        }

        internal Table<InboundArticle> InboundArticleTable
        {
            get { return this.GetTable(ref this.inboundArticleTable); }
        }

        internal Table<DeleteFile> DeleteFileTable
        {
            get { return this.GetTable(ref this.deleteFileTable); }
        }

        internal Table<TEntity> GetTableFast<TEntity>() where TEntity : class
        {
            var entityType = typeof(TEntity);
            ITable table;

            if (!this.tables.TryGetValue(entityType, out table))
            {
                table = this.GetTable(entityType);
                this.tables.Add(entityType, table);
            }

            return (Table<TEntity>)table;
        }

        internal Root Root
        {
            get
            {
                this.AssertNotDisposedCache();
                return this.root;
            }
        }

        internal void AssertSendReceiveNotInProgress()
        {
            if (this.cache != null)
            {
                this.cache.AssertSendReceiveNotInProgress();
            }
        }

        internal void AssertNotDisposedCache()
        {
            if (this.isDisposed)
            {
                throw new ObjectDisposedException(this.cache.ToString());
            }
        }

        internal void AssertNotDisposedOtherObjects()
        {
            if (this.isDisposed)
            {
                throw new InvalidOperationException("The parent Cache object has been disposed.");
            }
        }

        internal void InsertOnSubmit<TEntity>(TEntity entity) where TEntity : class
        {
            this.GetTableFast<TEntity>().InsertOnSubmit(entity);
        }

        internal void Attach<TEntity>(TEntity entity) where TEntity : class
        {
            this.GetTableFast<TEntity>().Attach(entity);
        }

        internal void Attach<TEntity>(TEntity entity, TEntity original) where TEntity : class
        {
            this.GetTableFast<TEntity>().Attach(entity, original);
        }

        internal void DeleteOnSubmit<TEntity>(TEntity entity) where TEntity : class
        {
            this.GetTableFast<TEntity>().DeleteOnSubmit(entity);
        }

        internal void ScheduleAfterCommitAction(Action action)
        {
            this.afterCommitActions[action] = false;
        }

        internal void SetReopenAfterCommit()
        {
            this.reopenAfterCommit = true;
        }

        internal bool ReopenAfterCommit
        {
            get { return this.reopenAfterCommit; }
        }

        internal static void CreateDatabase(FileIOProvider fileIOProvider)
        {
            fileIOProvider.CreateDirectory(string.Empty);

            if (!fileIOProvider.IsEmptyDirectory(string.Empty))
            {
                throw new ArgumentException("The specified directory is not empty.");
            }

            using (var database = new Database(fileIOProvider))
            {
                database.CreateDatabase();
#if !WINDOWS_PHONE
                database.AddIndexes();
                database.ExecuteCommand("INSERT INTO __VERSION(SchemaVersion) VALUES(0)");
#endif
                database.InsertOnSubmit(new Root());
                database.SubmitChanges();
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Releases the unmanaged resources used by the <see cref="Database"/> and optionally releases the
        /// managed resources.</summary>
        protected sealed override void Dispose(bool disposing)
        {
            this.isDisposed = true;
            base.Dispose(disposing);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private Database(FileIOProvider fileIOProvider) : base(GetConnectionString(fileIOProvider))
        {
            this.fileIOProvider = fileIOProvider;
        }

        private Table<TEntity> GetTable<TEntity>(ref Table<TEntity> storage) where TEntity : class
        {
            if (storage == null)
            {
                storage = this.GetTableFast<TEntity>();
            }

            return storage;
        }

        private static string GetConnectionString(FileIOProvider fileIOProvider)
        {
            return fileIOProvider.GetDatabaseConnectionString("Cache.sdf");
        }

#if !WINDOWS_PHONE
        private void AddIndexes()
        {
            this.AddIndexes<Version>();
            this.AddIndexes<Root>();
            this.AddIndexes<Host>();
            this.AddIndexes<OutboundArticle>();
            this.AddIndexes<Group>();
            this.AddIndexes<InboundArticle>();
        }

        private void AddIndexes<T>() where T : class
        {
            var indexAttributes = (IndexAttribute[])typeof(T).GetCustomAttributes(typeof(IndexAttribute), true);
            var tableAttribute = ((TableAttribute[])typeof(T).GetCustomAttributes(typeof(TableAttribute), true))[0];

            foreach (var indexAttribute in indexAttributes)
            {
                var tableName = string.IsNullOrEmpty(tableAttribute.Name) ? typeof(T).Name : tableAttribute.Name;
                var columnNames = indexAttribute.Columns.Split(',');
                var indexName = string.IsNullOrEmpty(indexAttribute.Name) ?
                    "IX_" + (indexAttribute.IsUnique ? "UNIQUE_" : string.Empty) + tableName + "_" +
                        string.Join("_", columnNames).Replace(' ', '_') :
                    indexAttribute.Name;
                var quotedColumns = string.Join(
                    ",", columnNames.Select(column => QuoteColumn(column)));
                var commandString = "CREATE " + (indexAttribute.IsUnique ? "UNIQUE " : string.Empty) +
                    "INDEX " + indexName + " ON [" + tableName + "](" + quotedColumns + ")";

                this.ExecuteCommand(commandString);
            }
        }

        private static string QuoteColumn(string column)
        {
            var parts = column.Split(' ');

            if (parts.Length == 1)
            {
                return '[' + parts[0] + ']';
            }
            else
            {
                return '[' + parts[0] + "] " + parts[1];
            }
        }
#endif
    }
}
